/*
 * The Yard Utilties - http://www.theyard.net/
 * 
 * Copyright (c) 2008 by Vlideshow, Inc..  All Rights Resrved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free Software 
 * Foundation; either version 2.1 of the License, or (at your option) any later 
 * version. 
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this library; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 */
package net.theyard.components.netconnectionsm
{
  import net.theyard.components.netstreamsm.NetStreamStateMachine;
  import net.theyard.components.sm.StateMachine;
  import net.theyard.components.Utils;

  import flash.events.AsyncErrorEvent;
  import flash.events.IOErrorEvent;
  import flash.events.NetStatusEvent;
  import flash.events.SecurityErrorEvent;
  import flash.net.NetConnection;
  import flash.net.ObjectEncoding;
  import flash.net.Responder;

  /**
   * Represents a connection to a server.  It connects over the network,
   *
   * <p>
   * The state transition of a successful lifecycle goes
   * </p><p>
   * DISCONNECTED -> CONNECTING -> CONNECTED -> DISCONNECTING -> DISCONNECTED
   * </p><p>
   * At any point the object may receive an an asynchronous event from the
   * server that disconnects it and users of this class should expect that.
   * They can listen for StateChange events that will inform them of these
   * state changes.
   * </p>
   */
  /**
   * This is a state machine wrapper for the flash.net.NetConnection class. 
   *
   * <p>
   * It's goal is to abstract away all the NetStatus events and
   * instead represent the state of the NetConnection in simple, easy
   * to understand concepts.
   * </p>
   * <p>
   * They are:
   * <ul>
   * <li>
   * <a href="#STATE_DISCONNECTED">STATE_DISCONNECTED</a>: We are currently disconnected.
   * </li><li>
   * <a href="#STATE_CONNECTING">STATE_CONNECTING</a>: We are attempting to connect.
   * </li><li>
   * <a href="#STATE_CONNECTED">STATE_CONNECTED</a>: We are successfully connected.
   * </li><li>
   * <a href="#STATE_DISCONNECTING">STATE_DISCONNECTING</a>: We are attempting to disconnect.
   * </li>
   * </ul>
   * </p>
   * <p>
   * To use you call (mostly) the same methods you'd call on a
   * flash.net.NetConnection object.
   * </p>
   * <p>
   * You then register EventListers for net.theyard.components.sm.events.StateChangeEvents when you want to see changes, or just ask the object for its current state.
   * </p>
   * <p>
   * For example:
   * <code>
<pre>
package net.theyard.components.netconnectionsm
{
  import net.theyard.components.sm.events.StateChangeEvent;
  import net.theyard.components.netconnectionsm.NetConnectionStateMachine;

  public class SampleUseCase
  {
    private var ncsm : NetConnectionStateMachine = null;
    public function SampleUseCase()
    {
      ncsm = new NetConnectionStateMachine();
      ncsm.addEventListener(StateChangeEvent.STATE_CHANGE,
          this.onConnectionStateChange);
      ncsm.connect("rtmp://localhost/application");
    }

    public function onConnectionStateChange(event:StateChangeEvent):void
    {
      switch(event.getState())
      {
        case NetConnectionStateMachine.STATE_CONNECTING:
          trace("connecting...");
          break;
        case NetConnectionStateMachine.STATE_CONNECTED:
          trace("connected");
          break;
        case NetConnectionStateMachine.STATE_DISCONNECTING:
          trace("disconnecting...");
          break;
        case NetConnectionStateMachine.STATE_DISCONNECTED:
          trace("disconnected");
          break;
        default:
          trace("will never get here...");
          break;
      }
    }
  }
}
</pre>
   * </code>
   * </p>
   *

   * @see net.theyard.components.sm.events.StateChangeEvent
   * @see flash.net.NetConnection
   * @see flash.net.NetStream
   * @see net.theyard.components.netstreamsm.NetStreamStateMachine
   * @see SampleUseCase
   *
   */
  public class NetConnectionStateMachine extends StateMachine
  {
    public static const STATE_DISCONNECTED :String="NetConnectionStateMachine.State.Disconnected";
    public static const STATE_CONNECTING   :String="NetConnectionStateMachine.State.Connecting";
    public static const STATE_CONNECTED    :String="NetConnectionStateMachine.State.Connected";
    public static const STATE_DISCONNECTING:String="NetConnectionStateMachine.State.Disconnecting";

    private var _uri:String=null;
    private var _nc:NetConnection=null;
    private var _delegate:NetConnectionCallDelegate;

    /**
     * Initializes the object
     */
    public function NetConnectionStateMachine()
    {
      super();
      // Set up all the states we need to support
      this.addState(new ConnectingState(this));
      this.addState(new ConnectedState(this));
      this.addState(new DisconnectingState(this));
      this.addState(new DisconnectedState(this));
      this.setStartingState(STATE_DISCONNECTED);


      _nc = new NetConnection();
      _nc.objectEncoding = ObjectEncoding.AMF0;

      // we default to our own delegate here, but
      // users can override it with any subclass.
      // this delegate just throws an error if any
      // callback is executed.
      _delegate = new NetConnectionCallDelegate();
      _nc.client = _delegate;

      // Set up event handlers.
      _nc.addEventListener( NetStatusEvent.NET_STATUS, netStatus );
      _nc.addEventListener( AsyncErrorEvent.ASYNC_ERROR, netASyncError );
      _nc.addEventListener( SecurityErrorEvent.SECURITY_ERROR, netSecurityError );
      _nc.addEventListener( IOErrorEvent.IO_ERROR, netIOError );
    }

    /**
     * Calls addHeader on the NetConnection
     *
     * @see NetConnection#addHeader()
     */
    public function addHeader(
        operation : String, mustUnderstand : Boolean = false,
        param : Object = null) : void
    {
      _nc.addHeader(operation, mustUnderstand, param);
    }

    /**
     * Attempts to connect if in the right state.
     *
     * If called when already connected, but to the same URI, the call is ignored.
     * Otherwise this method will cause us to disconnect and then try to connect
     * to the new server.
     *
     * @see NetConnection#connect()
     */
    public function connect(command: String, ...args ) : void
    {
      if (command == null || command.length==0)
      {
        throw new ArgumentError(
            "Must pass a non-zero length string to this function for command");
      }

      args.unshift(command);
      args.unshift(_nc);
      args.unshift(state);
      args.unshift(ConnectionState(state).connect);
      this.deferCallUntilStableState.apply(this, args);

      //ConnectionState(state).connect(_nc, command, args);
    }

    /**
     * Sets the object that will be used if the server tries to execute a
     * method on this client.
     *
     * @see NetConnectionCallDelegate
     * @see NetConnection#client
     */
    public function set client(delegate : NetConnectionCallDelegate) : void
    {
      _delegate = delegate;
      _nc.client = _delegate;
    }

    /**
     * Gets the object that will be used if the caller tries to execute a
     * method on this client.
     *
     * @see NetConnection#client
     */
    public function get client() : NetConnectionCallDelegate
    {
      return _delegate;
    }

    /**
     * Call a method on the server.
     *
     * @see NetConnection#call()
     */
    public function call(aCommand:String, aResultFunc: Function, aStatusFunc: Function, ...aArgs) : void
    {
      var statFunc:Function = function(aObj:Object):void 
      {
        Utils.ytrace("call status for [" + aCommand + "]: " + Utils.yAssocArrayAsString(aObj));
        if (aStatusFunc != null)
        {
          aStatusFunc(aObj);
        }
      }

      var resFunc:Function = function(aObj:Object):void 
      {
        Utils.ytrace("call result for [" + aCommand + "]: " + Utils.yAssocArrayAsString(aObj));
        if (aResultFunc != null)
        {
          aResultFunc(aObj);
        }
      }
      var res:Responder = new Responder(resFunc, statFunc);

      aArgs.unshift(res);
      aArgs.unshift(aCommand);

      _nc.call.apply(nc, aArgs);

    }

    /**
     * Attempts to close an active connection.
     *
     * Will be ignored if we're not currently connected.
     *
     * @see NetConnection.close()
     */
    public function close() : void
    {
      // Close the NetConnection.
      this.deferCallUntilStableState(ConnectionState(state).disconnect,
          state, nc);
    }

    /**
     * Create a new NetStreamStateMachine attached to this connection.
     *
     * @see net.theyard.components.netstreamsm.NetStreamStateMachine
     */
    public function getNewNetStream() : NetStreamStateMachine
    {
      return new NetStreamStateMachine(this);
    }

    /**
     * Are we currently connected?
     *
     * @return Are we connected?
     */
    public function get connected() : Boolean
    {
      return state.toString() == NetConnectionStateMachine.STATE_CONNECTED;
    }

    /**
     * @see NetConnection#connectedProxyType
     */
    public function get connectedProxyType() : String
    {
      return _nc.connectedProxyType;
    }

    /**
     * @see NetConnection#objectEncoding
     */
    public function get objectEncoding() : uint
    {
      return _nc.objectEncoding;
    }

    /**
     * @see NetConnection#objectEncoding
     */
    public function set objectEncoding(encoding:uint):void
    {
      _nc.objectEncoding = encoding;
    }

    /**
     * @see NetConnection#proxyType
     */
    public function get proxyType():String
    {
      return _nc.proxyType;
    }

    /**
     * @see NetConnection#proxyType
     */
    public function set proxyType(type:String):void
    {
      _nc.proxyType = type;
    }

    /**
     * Get the URI we last attempted to connect to.
     *
     * @return the URI
     *
     * @see NetConnection#uri
     */
    public function get uri() : String
    {
      return _uri;
    }

    /**
     * @see NetConnection#usingTLS
     */
    public function get usingTLS() : Boolean
    {
      return _nc.usingTLS;
    }

    /**
     * Get the underlying NetConnection we wrap.
     *
     * Be careful with this; if you start calling methods directly on
     * the NetConnection you are likely to make this state machine get things
     * wrong.  Better to use a method directly on this object.
     *
     * @return The NetConnection
     *
     * @see NetConnection
     */
    public function get nc() : NetConnection
    {
      return _nc;
    }

    internal function setUri(uri:String) : void
    {
      this._uri = uri;
    }

    /**
     * This method is called by the underlying NetConnection whenever a NetStatus event occurs.
     *
     * If you have a need to see the actual events we wrap, derive from this class
     * and override this method (but forward back here so we can do our work).
     *
     * @param event The event that occurred.
     */
    protected function netStatus( event : NetStatusEvent ) : void
    {
      // Pass NetStatusEvent to SetupConnectionCommand.
      // Only respond to netconnection events
      var code:String = event.info.code;

      switch (code)
      {
        case NetStatus.CONNECTION_CONNECT_APPSHUTDOWN:
        case NetStatus.CONNECTION_CONNECT_CLOSED:
        case NetStatus.CONNECTION_CONNECT_FAILED:
        case NetStatus.CONNECTION_CONNECT_INVALIDAPP:
        case NetStatus.CONNECTION_CONNECT_REJECTED:
          ConnectionState(state).network_disconnected(_nc);
          break;
        case NetStatus.CONNECTION_CONNECT_SUCCESS:
          ConnectionState(state).network_connected(_nc);
          break;
        default:
          Utils.ytrace("Ignoring: " + Utils.yAssocArrayAsString(event.info));
          break;
      }
    }

    /**
     * This method is called by the underlying NetConnection whenever a SecurityErrorEvent event occurs.
     *
     * If you have a need to see the actual events we wrap, derive from this class
     * and override this method (but forward back here so we can do our work).
     *
     * @param event The event that occurred.
     */
    protected function netSecurityError( event : SecurityErrorEvent ) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(event));
      this.dispatchEvent(event);

    }

    /**
     * This method is called by the underlying NetConnection whenever a IOErrorEvent event occurs.
     *
     * If you have a need to see the actual events we wrap, derive from this class
     * and override this method (but forward back here so we can do our work).
     *
     * @param event The event that occurred.
     */
    protected function netIOError( event : IOErrorEvent ) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(event));
      this.dispatchEvent(event);
    }

    /**
     * This method is called by the underlying NetConnection whenever a AsyncErrorEvent event occurs.
     *
     * If you have a need to see the actual events we wrap, derive from this class
     * and override this method (but forward back here so we can do our work).
     *
     * @param event The event that occurred.
     */
    protected function netASyncError( event : AsyncErrorEvent ) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(event));
      this.dispatchEvent(event);
    }
  }
}

